// ValidationAspects
// Written by Mike Saunders
// Released under Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
using ValidationAspects;
using ValidationAspects.Factories;
using ValidationAspects.Sdk;

namespace TestValidationAspects
{
	[TestFixture]
	public class TestPropertyValidationRegistry
	{
		[SetUp]
		public void SetUp()
		{
			TestHelpers.ResetFramework();
			Emit<string>.Clear();
		}

		[Test]
		public void Test_FactoryDiscoveryFunc()
		{
			Test.Throws(() => TypeOf<Dummy>.GetProperty(x => x.Total).ResolveValidator().Invoke(-1.0m, new MockValidationContext()));
			Assert.IsNull(TypeOf<Dummy>.GetProperty(x => x.NoDefValidators).ResolveValidator());
		}

		#region Test ClearCache
		[Test]
		public void Test_ClearCache()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoDefValidators);
			propertyInfo.ReplaceValidation<string>((v, c) => { });

			Assert.IsNotNull(propertyInfo.ResolveValidator());

			PropertyValidationRegistry.ClearCache();

			Assert.IsNull(propertyInfo.ResolveValidator());
		}
		#endregion

		#region Test RevertValidation
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_RevertValidation_throws_for_null_target_param()
		{
			PropertyValidationRegistry.RevertValidation(null);
		}

		[Test]
		public void Test_RevertValidation_throws_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Test.Throws<ValidationException>(() => propertyInfo.RevertValidation(),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_RevertValidation_throws_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Test.Throws<ValidationException>(() => propertyInfo.RevertValidation(),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_RevertValidation()
		{
			// test for coverage
			TypeOf<Dummy>.GetProperty(x => x.Total).RevertValidation();
		}
		#endregion

		#region Test ReplaceValidation
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidation_throws_for_null_target_param()
		{
			PropertyValidationRegistry.ReplaceValidation(null, Enumerable.Empty<IValidatorFactory>());
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidation_throws_for_null_factories_param()
		{
			TypeOf<Dummy>.GetProperty(x => x.Total).ReplaceValidation(null);
		}

		[Test]
		public void Test_ReplaceValidation_throws_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Test.Throws<ValidationException>(() => propertyInfo.ReplaceValidation(Enumerable.Empty<IValidatorFactory>()),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_ReplaceValidation_throws_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Test.Throws<ValidationException>(() => propertyInfo.ReplaceValidation(Enumerable.Empty<IValidatorFactory>()),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_ReplaceValidation()
		{
			var property = TypeOf<Dummy>.GetProperty(x => x.Total);

			MockValidatorFactory<decimal> factory = new MockValidatorFactory<decimal>();
			property.ReplaceValidation(new[] {factory});

			IValidationContext context = new MockValidationContext();
			property.ResolveValidator().Invoke(1.23m, context);

			Assert.AreEqual(1.23m, factory.Value);
			Assert.AreSame(context, factory.Context);
		}
		#endregion

		#region Test ReplaceValidationT
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidationT_throws_for_null_target_param()
		{
			PropertyValidationRegistry.ReplaceValidation<object>(null, (v, c) => { });
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ReplaceValidationT_throws_for_null_action_param()
		{
			TypeOf<Dummy>.GetProperty(x => x.Total).ReplaceValidation<decimal>(null);
		}

		[Test]
		public void Test_ReplaceValidationT_throws_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Test.Throws<ValidationException>(() => propertyInfo.ReplaceValidation<double>((v, c) => { }),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_ReplaceValidationT_throws_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Test.Throws<ValidationException>(() => propertyInfo.ReplaceValidation<double>((v, c) => { }),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_ReplaceValidationT()
		{
			var property = TypeOf<Dummy>.GetProperty(x => x.Total);

			decimal valueFromValidator = 0;
			IValidationContext contextFromValidator = null;
			property.ReplaceValidation<decimal>((v, c) =>
			                                    {
			                                    	valueFromValidator = v;
			                                    	contextFromValidator = c;
			                                    });

			IValidationContext context = new MockValidationContext();
			property.ResolveValidator().Invoke(1.23m, context);

			Assert.AreEqual(1.23m, valueFromValidator);
			Assert.AreSame(context, contextFromValidator);
		}
		#endregion

		#region Test AddValidation
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidation_throws_for_null_target_param()
		{
			PropertyValidationRegistry.AddValidation(null, Enumerable.Empty<IValidatorFactory>());
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidation_throws_for_null_factories_param()
		{
			TypeOf<Dummy>.GetProperty(x => x.Total).AddValidation(null);
		}

		[Test]
		public void Test_AddValidation_throws_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Test.Throws<ValidationException>(() => propertyInfo.AddValidation(Enumerable.Empty<IValidatorFactory>()),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_AddValidation_throws_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Test.Throws<ValidationException>(() => propertyInfo.AddValidation(Enumerable.Empty<IValidatorFactory>()),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_AddValidation()
		{
			var property = TypeOf<Dummy>.GetProperty(x => x.Total);

			MockValidatorFactory<decimal> factory = new MockValidatorFactory<decimal>();
			property.AddValidation(new[] {factory});

			IValidationContext context = new MockValidationContext();
			property.ResolveValidator().Invoke(1.23m, context);

			Assert.AreEqual(1.23m, factory.Value);
			Assert.AreSame(context, factory.Context);
		}
		#endregion

		#region Test AddValidationT
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidationT_throws_for_null_target_param()
		{
			PropertyValidationRegistry.AddValidation<object>(null, (v, c) => { });
		}

		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_AddValidationT_throws_for_null_actions_param()
		{
			TypeOf<Dummy>.GetProperty(x => x.Total).AddValidation<decimal>(null);
		}

		[Test]
		public void Test_AddValidationT_throws_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Test.Throws<ValidationException>(() => propertyInfo.AddValidation<double>((v, c) => { }),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_AddValidationT_throws_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Test.Throws<ValidationException>(() => propertyInfo.AddValidation<double>((v, c) => { }),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_AddValidationT()
		{
			var property = TypeOf<Dummy>.GetProperty(x => x.Total);

			decimal valueFromValidator = 0;
			IValidationContext contextFromValidator = null;
			property.AddValidation<decimal>((v, c) =>
			                                {
			                                	valueFromValidator = v;
			                                	contextFromValidator = c;
			                                });

			IValidationContext context = new MockValidationContext();
			property.ResolveValidator().Invoke(1.23m, context);

			Assert.AreEqual(1.23m, valueFromValidator);
			Assert.AreSame(context, contextFromValidator);
		}
		#endregion

		#region Test ResolveValidator
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_ResolveValidator_throws_for_null_target_param()
		{
			PropertyValidationRegistry.ResolveValidator(null);
		}

		[Test]
		public void Test_ResolveValidator_throws_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Test.Throws<ValidationException>(() => propertyInfo.ResolveValidator(),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_ResolveValidator_throws_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Test.Throws<ValidationException>(() => propertyInfo.ResolveValidator(),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_ResolveValidator_throws_if_validator_construction_fails()
		{
			PropertyInfo propertyInfo = TypeOf<InvalidValidatorDecoration>.GetProperty(x => x.Id);
			Test.Throws<ValidationException, ValidationException>(() => propertyInfo.ResolveValidator(),
			                                                      string.Format("Failed to construct validator for [type:{0}] [property:{1}].", propertyInfo.DeclaringType, propertyInfo.Name));
		}

		[Test]
		public void Test_ResolveValidator_returns_null_for_no_validators()
		{
			var property = TypeOf<Dummy>.GetProperty(x => x.Total);
			property.ReplaceValidation(Enumerable.Empty<IValidatorFactory>());
			Assert.IsNull(property.ResolveValidator());
		}

		#endregion

		#region Test CanValidate
		[Test]
		[ExpectedException(typeof(ArgumentNullException))]
		public void Test_CanValidate_throws_for_null_target_param()
		{
			PropertyValidationRegistry.CanValidate(null);
		}

		[Test]
		public void Test_CanValidate_returns_false_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Assert.IsFalse(propertyInfo.CanValidate());
		}

		[Test]
		public void Test_CanValidate_returns_false_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Assert.IsFalse(propertyInfo.CanValidate());
		}

		[Test]
		public void Test_CanValidate_returns_true_for_validatable_property()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.Total);
			Assert.IsTrue(propertyInfo.CanValidate());
		}
		#endregion

		#region Test EmitValidation
		[Test]
		public void Test_EmitValidation_for_multiple_emit_registered_factories()
		{
			Emit<string>.For<Minimum>(f => "emit Minimum:" + f.Value);
			Emit<string>.For<MockValidatorFactory<decimal>>(f => "emit MockFactory:" + f.Value);

			PropertyInfo property = TypeOf<Dummy>.GetProperty(x => x.Total);
			property.AddValidation(new[] {new MockValidatorFactory<decimal> {Value = 1.23m}});

			IEnumerable<string> emitValidation = property.EmitValidation<string>();
			string[] emittedStrings = emitValidation.OrderBy(s => s).ToArray();

			Assert.AreEqual(2, emittedStrings.Length);
			Assert.AreEqual("emit Minimum:" + 0, emittedStrings[0]);
			Assert.AreEqual("emit MockFactory:" + 1.23m, emittedStrings[1]);
		}

		[Test]
		public void Test_EmitValidation_for_where_not_all_factories_are_emit_registered()
		{
			Emit<string>.For<Minimum>(f => "emit Minimum:" + f.Value);

			PropertyInfo property = TypeOf<Dummy>.GetProperty(x => x.Total);
			property.AddValidation(new[] {new MockValidatorFactory<decimal> {Value = 1.23m}});

			IEnumerable<string> emitValidation = property.EmitValidation<string>();
			string[] emittedStrings = emitValidation.OrderBy(s => s).ToArray();

			Assert.AreEqual(1, emittedStrings.Length);
			Assert.AreEqual("emit Minimum:" + 0, emittedStrings[0]);
		}

		[Test]
		public void Test_EmitValidation_for_no_factories_are_emit_registered()
		{
			IEnumerable<string> emitValidation = TypeOf<Dummy>.GetProperty(x => x.Total).EmitValidation<string>();
			Assert.AreEqual(0, emitValidation.Count());
		}

		[Test]
		public void Test_EmitValidation_throws_for_property_with_no_setter()
		{
			PropertyInfo propertyInfo = TypeOf<Dummy>.GetProperty(x => x.NoSetter);
			Test.Throws<ValidationException>(() => propertyInfo.EmitValidation<string>().Count(),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}

		[Test]
		public void Test_EmitValidation_throws_for_property_with_abstract_setter()
		{
			PropertyInfo propertyInfo = TypeOf<AbstractSetter>.GetProperty(x => x.Id);
			Test.Throws<ValidationException>(() => propertyInfo.EmitValidation<string>().Count(),
			                                 string.Format("Validation cannot be applied to property [name:{0}] [type:{1}]. Property does not define a non abstract set method.", propertyInfo.Name,
			                                               propertyInfo.DeclaringType));
		}
		#endregion

		#region Mocks
		public class Dummy
		{
			[Minimum(0)]
			public decimal Total { get; set; }

			public double NoSetter
			{
				get { return 0; }
			}

			public string NoDefValidators { get; set; }
		}

		public class InvalidValidatorDecoration
		{
			[Minimum(0)]
			public string Id { get; set; }
		}

		public abstract class AbstractSetter
		{
			[Minimum(0)]
			public abstract double Id { get; set; }
		}
		#endregion
	}
}